<?php

Raise::load('core.RaiseObject');
Raise::load('core.RaiseSettings');
Raise::load('core.RaiseHttpResponse');
Raise::load('core.RaiseHttpRequest');
Raise::load('core.RaiseHttpClient');
Raise::load('core.RaisePluginManager');
Raise::load('core.RaiseSession');
Raise::load('core.RaiseCsrfProtect');
Raise::load('core.RaiseDefaultExceptionHandler');
Raise::load('core.RaiseUserFactory');
Raise::load('core.RaisePath');
Raise::load('core.RaiseAppPackageLoader');
Raise::load('core.RaiseDateTime');
Raise::load('core.RaiseTimeSpan');
Raise::load('core.RaiseCollection');
Raise::load('core.RaisePage');
Raise::load('core.RaisePostback');
Raise::load('core.RaiseVariable');
Raise::load('core.RaiseUrlRoute');

/**
 * RaiseApplication class
 * entry point for the application to work.
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package Raise.Core
 * @since 1.2
 */
class RaiseApplication extends RaiseObject {

    /**
     * The name of this RaiseApplication
     * Allowed characters include: alpha numeric, dots, hypens
     * The name must start and end with alpha numeric characters
     * Length of the name must be between 6 to 30 allowed characters.
     * The allowed characters can be expressed as regular expression: ^[a-zA-Z0-9]{1}[a-zA-Z0-9.-]{4,28}[a-zA-Z0-9]{1}$
     * @var string
     */
    private $name;

    /**
     * The configuration file in use
     * @var RaiseSettings
     */
    private $settings;

    /**
     * The HTTP response of the application to the request.
     * @var RaiseHttpResponse
     */
    private $httpResponse;

    /**
     * The HTTP request to the application
     * @var RaiseHttpRequest
     */
    private $httpRequest;

    /**
     * The client accessing the application via HTTP
     * @var RaiseHttpClient
     */
    private $httpClient;

    /**
     * The RaisePluginManager handling plugins in the application
     * @var RaisePluginManager
     */
    private $plugin;

    /**
     * The Session used by this application
     * @var RaiseSession
     */
    private $session;

    /**
     * The RaiseCsrfProtect that will provide CSRF protection for the application
     * @var RaiseCsrfProtect
     */
    private $csrfProtect;

    /**
     * The RaiseAccessPermissions that checks for access and rules of users and application resources
     * @var RaiseAccessPermissions
     */
    private $accessControl;

    /**
     * The RaiseDefaultExceptionHandler that will handle all exceptions in this application that were not caught
     * @var RaiseDefaultExceptionHandler
     */
    private $exceptionHandler;

    /**
     * Creates the application object with the application name.
     * @param string|RaiseSettings $name The name of the application or the RaiseSettings to use. See $this->name()
     */
    function __construct($name) {
        if ($name instanceof RaiseSettings) {
            $this->settings = $name;
        } else {
            $this->name($name);
        }

        $this->initialize();
    }

    /**
     * Get the name of the RaiseApplication
     * Allowed characters include: alpha numeric, dots, hypens. The name must start and end with alpha numeric characters.
     * Length of the name must be between 4 to 30 allowed characters.
     * The allowed characters can be expressed as regular expression: ^[a-zA-Z0-9]{1}[a-zA-Z0-9.-]{4,28}[a-zA-Z0-9]{1}$
     * @param string $n (optional) This can be only called in the constructor to validate and set the name.
     * @return string
     */
    public function name($n = null) {
        if (func_num_args() == 1) {
            if ($n !== null) {
                $i = preg_match('`^[a-zA-Z0-9]{1}[a-zA-Z0-9.-]{2,28}[a-zA-Z0-9]{1}$`is', $n);
                if ($i == 0) {
                    throw new RaiseInvalidArgumentException('Invalid RaiseApplication name entered. RaiseApplication name must match the regular expression "^[a-zA-Z0-9]{1}[a-zA-Z0-9.-]{4,28}[a-zA-Z0-9]{1}$"');
                }
            }
            $this->name = $n;
        }
        return $this->name;
    }

    /**
     * Get the RaiseHttpResponse to the RaiseHttpRequest
     * @return RaiseHttpResponse
     */
    public function response() {
        return $this->httpResponse;
    }

    /**
     * Get the RaiseHttpRequest that is received by this application
     * @return RaiseHttpRequest
     */
    public function request() {
        return $this->httpRequest;
    }

    /**
     * Get the RaiseHttpClient that is accessing this application via HTTP
     * @return RaiseHttpClient
     */
    public function client() {
        return $this->httpClient;
    }

    /**
     * Get the RaisePluginManager that is handling plugins for this application
     * @return RaisePluginManager
     */
    public function plugin() {
        return $this->plugin;
    }

    /**
     * Get the RaiseSession that is currently used by the application
     * @return RaiseSession
     */
    public function session() {
        return $this->session;
    }
    
    /**
     *  Get or set the permission checking RaiseAccessPermissions object for this application
     * @param RaiseAccessPermissions $ac
     * @return RaiseAccessPermissions 
     */
    public function accessControl($ac = null){
        if($ac instanceof RaiseAccessPermissions){
            $this->accessControl = $ac;
        }
        return $this->accessControl;
    }

    /**
     * Get the identifier of the user who is currently accessing the application
     * @return mixed
     */
    public function user($user = null) {
        static $factory = false;
        if (!$factory) {
            $factory = new RaiseUserFactory($this->session);
        }
        if(func_num_args() == 1){
            $factory->user($user);
        }
        return $factory->user();
    }

    /**
     * Get the RaiseExceptionHandler that will handle all exceptions in this application
     * @return RaiseExceptionHandler
     */
    public function exceptionHandler() {
        return $this->exceptionHandler;
    }

    /**
     * Get the RaiseCsrfProtect that provides CSRF protection for this application
     * @return RaiseCsrfProtect
     */
    public function csrfProtect() {
        return $this->csrfProtect;
    }

    /**
     * Return the settings or set which configuration file to use as settingsfor the application
     * @param string (optional) Configuration file to use, without pathname or extension.
     * @return RaiseSettings The settings used for this application
     */
    public function settings($cfg = null) {
        if (func_num_args() == 1 && $cfg) {
            $core_dir = RaisePath::pathInfo(__FILE__, RaisePath::DIRECTORY) . RaisePath::directorySeparator();
            $configfile = RaisePath::resolve($core_dir . '..' . RaisePath::directorySeparator() . 'config' . RaisePath::directorySeparator() . $cfg . '.ini');
            $this->settings = new RaiseSettings($configfile);
        }
        return $this->settings;
    }

    /**
     * Get the application root path
     * @return string
     */
    public function rootPath() {
        static $appRoot = false;
        if (!$appRoot) {
            $appRoot = $this->settings()->get('phpRaise', 'appRoot');
            if (!$appRoot) {
                return null;
            }
            $root = RaisePath::resolve(RaisePath::phpRaisePath() . RaisePath::directorySeparator() . $appRoot);
            if ($root) {
                $appRoot = $root;
            }
        }
        return $appRoot;
    }

    private function loadPackages() {

        $loader = new RaiseAppPackageLoader($this);
        $loader->loadPackage('app');
        $loader->loadPackage('dao');
        $loader->loadPackage('class');
        $loader->loadPackage('model');
        $loader->loadPackage('control');
        $loader->loadPackage('library');
        $loader->loadTemplates();
        /**
         * load the plugins
         */
        if ($this->rootPath()) {
            $this->plugin = new RaisePluginManager(RaisePath::combine($this->rootPath(), 'plugin'));
        } else {
            $this->plugin = new RaisePluginManager();
        }
    }

    /**
     * Initializes the application.
     */
    private function initialize() {
        /**
         * Set configuration file if no configuration file was set by programmer
         */
        if (!($this->settings instanceof RaiseSettings)) {
            $this->settings($this->name());
        }

        $this->loadPackages();
        $this->plugin()->callHook('raise.application.initialize.start', $this);

        $this->httpRequest = RaiseHttpRequest::loadCurrent($this);
        $this->httpResponse = new RaiseHttpResponse($this);
        $this->httpClient = RaiseHttpClient::loadCurrent();

        /**
         * Load compatibility functions and tweaks
         * if required
         */
        if ($this->settings()->get('phpRaise', 'loadCompatibilityFunctions')) {
            Raise::load('core.RaiseCompatibility');
        }

        /**
         * Set the time zone as defined in settings
         */
        if ($this->settings()->get('phpRaise', 'timezone')) {
            date_default_timezone_set($this->settings()->get('phpRaise', 'timezone'));
        }

        /**
         * Set Session Garbage Collection settings
         */
        ini_set('session.gc_divisor', 100);
        if ($this->settings()->get('phpRaise', 'siteActivityLevel') > 0) {
            ini_set('session.gc_probability', ((11 - $this->settings()->get('siteActivityLevel')) / 10 * 100));
        }

        $this->exceptionHandler = new RaiseDefaultExceptionHandler($this);

        /**
         * Enable GZIP for HTTP
         * if set
         */
        if ($this->settings()->get('phpRaise', 'enableGzip')) {
            ini_set('zlib.output_compression', '1');
        }

        /**
         * Session start up procedure
         */
        if ($this->settings()->get('Session', 'store')) {
            $this->session = new RaiseSession(RaiseSession::loadStore($this->settings()));
        } else {
            $this->session = new RaiseSession();
        }
        $this->session->application($this);
        if ($this->settings()->get('Session', 'name')) {
            $this->session->name($this->settings()->get('Session', 'name'));
        }
        $timespan = $this->settings()->get('Session', 'timeSpan');
        if (!$timespan || $timespan <= 0) {
            $timespan = 3600;
        }
        $this->session->cookie()->expire(RaiseDateTime::now()->add(new RaiseTimeSpan($timespan)));
        $this->session->start();

        /**
         * Enable CSRF Protection if set
         */
        if ($this->settings()->get('phpRaise', 'enableCsrfProtection')) {
            $this->csrfProtect = new RaiseCsrfProtect($this);
            $this->csrfProtect->enable();
        }

        $this->plugin()->callHook('raise.application.initialize.end', $this);
    }

    /**
     * Check whether the current user has permission to access the resource
     * @param string $resource
     */
    private function checkPermissions($resource) {
        $ok = true;
        
        if(!($this->accessControl instanceof RaiseAccessPermissions)){
            if (class_exists('AppAccess')) {
                $this->accessControl = new AppAccess();
                $this->accessControl->initialize();
            }else{
                $this->accessControl = new RaiseAccessReset();
                $this->accessControl->initialize();
            }
        }
        $ok = $this->accessControl->check($this->user(), $resource);
        if (!$ok) {
            throw new RaiseUserAccessDeniedException('No permission to access resource \'' . $resource . '\'.');
        }
    }

    /**
     * Start the application using a specific page
     * @param string|RaisePage $page The page to load and start
     * @param RaiseCollection $args (optional) A collection of arguments passed into the page
     * @return RaisePage The page that was loaded and output.
     */
    public function startPage($page, $args = null) {
        $this->plugin()->callHook('raise.application.page.start', $this);

        if (RaiseVariable::isType($args, 'array')) {
            $args = new RaiseCollection($args);
        }

        if ($args && !($args instanceof RaiseCollection)) {
            throw new RaiseInvalidArgumentException('RaiseApplication::startPage() expecting $args to be of type RaiseCollection. Type \'' . RaiseVariable::typeInfo($args) . '\' given. ');
        }

        $p = null;
        if (RaiseVariable::isType($page, RaiseVariable::TYPESTRING)) {
            $pageClass = $page;
            if (!class_exists($pageClass)) {
                $classPath = RaisePath::combine($this->rootPath(), 'raise/page/' . $page . '.php');
                include($classPath);
            }
            if (class_exists($pageClass)) {
                $p = new $pageClass();
            }
        } elseif ($page instanceof RaisePage) {
            $p = $page;
        }

        if ($p instanceof RaisePage) {

            if ($this->request()->get()->count() > 0) {
                $p->params()->append($this->request()->get());
            }
            if ($args) {
                $p->params()->append($args);
            }

            $this->response()->headers()->remove('ETag');
            $this->response()->headers()->remove('Last-Modified');
            $this->response()->headers()->add('Expires', '0', true);

            $p->application($this);
            $p->load();
            $this->checkPermissions(get_class($p));
            $html = $p->output();
            $html = $this->csrfProtect()->prepare($html);
            $this->response()->body($html);
            $this->response()->output();
            $p->unload();
        } else {
            throw new RaiseInvalidRequestException('The RaisePage object of class name \'' . $page . '\' was not found.');
        }

        $this->plugin()->callHook('raise.application.page.end', $this);
        return $p;
    }

    /**
     * Starts the application with a specific controller
     * @param string $controller Name of the controller to execute
     * @param string $action The name of the action to execute
     * @param RaiseCollection $args (optional) A collection of arguments passed into the postback controller
     * @return RaisePostback The RaisePostback controller that was executed
     */
    public function startPostback($controller, $action, $args = null) {
        $this->plugin()->callHook('raise.application.postback.start', $this);

        if ($args && !($args instanceof RaiseCollection)) {
            throw new RaiseInvalidArgumentException('RaiseApplication::startPostback() expecting $args to be of type RaiseCollection. Type \'' . RaiseVariable::typeInfo($args) . '\' given. ');
        }

        $c = null;
        if (RaiseVariable::isType($controller, RaiseVariable::TYPESTRING)) {
            $ctrlClass = $controller;
            if (!class_exists($ctrlClass)) {
                $classPath = RaisePath::combine($this->rootPath(), 'raise/postback/' . $controller . '.php');
                include($classPath);
            }
            if (class_exists($ctrlClass)) {
                $c = new $ctrlClass($this);
            }
        } elseif ($controller instanceof RaisePostback) {
            $c = $controller;
        }

        if ($c instanceof RaisePostback) {
            $this->checkPermissions(get_class($c));

            if ($this->request()->post()->count() > 0) {
                $c->params()->append($this->request()->post());
            }
            if ($args) {
                $c->params()->append($args);
            }
            if ($c->validate()) {
                $c->run($action);
            } else {
                throw new RaiseInvalidRequestException('Request for \'' . $controller . '\' is invalid.');
            }
        } else {
            throw new RaiseInvalidRequestException('The RaisePostback controller of class name \'' . $controller . '\' was not found.');
        }

        $this->plugin()->callHook('raise.application.postback.end', $this);

        return $c;
    }

    /**
     * Do the neccessary routing functionalities and simulation
     * This method should only be called on the root index page.
     */
    public function doRouting() {
        $this->plugin()->callHook('raise.application.routing.start', $this);
        $url = $this->request()->get($this->settings()->get('phpRaise', 'enableRewrite') ? RaiseUrlRoute::KEY : 'p');
        if ($url == null) {
            $url == '';
        }
        $routes = RaiseUrlRoute::routes();
        $deckRoute = new RaiseUrlRoute('deck/$controller$/$action$', '', array('controller' => '[a-zA-Z0-9\.\-_]+', 'action' => '[a-zA-Z0-9\.\-_]+'));
        $routes->add('deckRoute', $deckRoute);

        foreach ($routes as $route) {
            $t = new RaiseTemplate($route->rewrite());
            $tokens = $t->tokens();
            foreach ($tokens as $a) {
                $v = $route->params()->get($a);
                if (!$v) {
                    $v = '*';
                }
                $t->fields()->add($a, '(?P<' . $a . '>' . $v . ')');
            }
            $matches = array();
            $i = preg_match('`^' . $t->parse() . '$`is', $url, $matches);
            if ($i) {
                $this->plugin()->callHook('raise.application.end.success', $this);
                $args = array();
                foreach ($tokens as $a) {
                    $args[$a] = $matches[$a];
                }
                if ($route == $deckRoute) {
                    $controller = $args['controller'];
                    $action = $args['action'];
                    unset($args['controller'], $args['action']);
                    $this->startPostback($controller . 'Postback', $action, $args);
                } else {
                    $this->startPage($route->actual() . 'Page', $args);
                }
                return;
            }
        }

        $this->plugin()->callHook('raise.application.routing.end.fail', $this);

        $ex = new RaisePageNotFoundException('The requested \'' . $url . '\' cannot be found.');
        $ex->setContext($this);
        throw $ex;
    }

}
